package com.kamistoat.meimeistore.commonsecurity.utils;

import com.alibaba.fastjson2.JSON;
import com.kamistoat.meimeistore.apisystem.feign.RemoteUserService;
import com.kamistoat.meimeistore.apisystem.model.LoginUser;
import com.kamistoat.meimeistore.commoncore.constant.CacheConstants;
import com.kamistoat.meimeistore.commoncore.constant.SecurityConstants;
import com.kamistoat.meimeistore.commoncore.utils.JwtUtils;
import com.kamistoat.meimeistore.commoncore.utils.ServletUtils;
import com.kamistoat.meimeistore.commoncore.utils.StringUtils;
import com.kamistoat.meimeistore.commoncore.utils.ip.IpUtils;
import com.kamistoat.meimeistore.commoncore.utils.uuid.IdUtils;
import com.kamistoat.meimeistore.commonredis.utils.RedissonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 用户令牌工具类。提供制作/缓存/验证用户令牌操作接口
 */
@Component
public class TokenUtils {
    @Autowired
    RedissonUtils redissonUtils;
    @Autowired
    RemoteUserService remoteUserService;

    /**
     * 毫秒与秒的转换
     */
    protected static final long MILLIS_SECOND = 1000;
    /**
     * 分钟与毫秒转换
     */
    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
    /**
     * 默认过期时间
     */
    private final static long expireTime = CacheConstants.EXPIRATION;
    /**
     * 在后端缓存的登录令牌的前缀
     */
    private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
    /**
     * 在后端缓存的用户核心信息令牌的前缀
     */
    private final static String USER_CORE = CacheConstants.USER_CORE_KEY;
    /**
     * 缓存中的用户令牌自动延期时的最小剩余有效期(毫秒)
     */
    private final static Long MILLIS_MINUTE_TEN = CacheConstants.REFRESH_TIME * MILLIS_MINUTE;

    /**
     * 创建用户令牌.包括用于自动登录的登录令牌，以及缓存用户核心信心的令牌
     *
     * @param coreLoginUser 在完成登陆后，从数据库中查询到的用户信息.此时的 coreLoginUser 只包含了用户的核心信息，可以直接作为用户核心信息令牌。
     *                      在此方法中还需制作后端登录令牌。两个令牌都会入缓存。
     * @return 根据用户令牌的 token/userId/userName 制作的Jwt数据，作为返回给前端的简化用户令牌。之后如何携带都是前端游览器考虑的问题，和后端无关。
     */
    public Map<String, Object> createToken(LoginUser coreLoginUser) {
        // 生成随机uuid作为token，是登录令牌的唯一标识。注意只有登录令牌需要uuid，核心信息令牌并不需要
        String token = IdUtils.fastUUID();
        // 获取LoginUser对应的数据库中SysUser的id
        Long userId = coreLoginUser.getUserid();
        // 获取LoginUser对应的数据库中SysUser的name
        String userName = coreLoginUser.getUsername();

        // 创建新的 LoginUser，只缓存登录信息，作为后端登录令牌
        LoginUser loginLoginUser = new LoginUser();

        // 向 后端登录令牌 中填充自动登录信息
        loginLoginUser.setToken(token);  //
        loginLoginUser.setUserid(userId);
        loginLoginUser.setUsername(userName);
        loginLoginUser.setIpaddr(IpUtils.getIpAddr());

        // 将 后端登录令牌 缓存，并刷新其过期时间
        refreshToken(loginLoginUser);

        // 将 核心信息令牌 缓存，并刷新过期时间。同时缓存 登录令牌 和 核心令牌，保证了每次登录必定得到最新的核心缓存
        refreshCoreLoginUser(coreLoginUser);

        // 抽取用户令牌的 token userId userName 来制作Jwt数据
        Map<String, Object> claimsMap = new HashMap<>();
        claimsMap.put(SecurityConstants.USER_KEY, token);
        claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
        claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);

        // 接口返回给前端的数据。并不是返回完整的用户令牌，而是仅仅返回Jwt数据
        // Jwt数据的名字写的是 access_token，这是因为 token可以作为用户令牌和Jwt的唯一标识。注意和上面String类型的token区分
        Map<String, Object> rspMap = new HashMap<>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expires_in", expireTime);
        return rspMap;
    }

    /**
     * 从前端发送的当前请求中获取 Jwt，然后根据 Jwt从后端缓存中获取 后端登录令牌
     *
     * @return 缓存中的 后端登录令牌
     */
    public LoginUser getLoginLoginUser() {
        return getLoginLoginUser(ServletUtils.getRequest());
    }

    /**
     * 从前端发送的当前请求中获取 Jwt，然后根据 Jwt从后端缓存中获取 核心信息令牌
     *
     * @return 缓存中的 核心信息令牌
     */
    public LoginUser getCoreLoginUser() {
        return getCoreLoginUser(ServletUtils.getRequest());
    }

    /**
     * 从指定请求中获取 Jwt，然后根据Jwt从后端缓存中获取 后端登录令牌
     *
     * @param httpServletRequest 指定的请求
     * @return 缓存中的 后端登录令牌
     */
    public LoginUser getLoginLoginUser(HttpServletRequest httpServletRequest) {
        // 从请求中获取裁剪后的 jwt数据
        String jwtToken = SecurityUtils.getJwtToken(httpServletRequest);
        return getLoginLoginUser(jwtToken);
    }

    /**
     * 从指定请求中获取 Jwt，然后根据Jwt从后端缓存中获取 核心信息令牌
     *
     * @param httpServletRequest 指定的请求
     * @return 缓存中的 核心信息令牌
     */
    public LoginUser getCoreLoginUser(HttpServletRequest httpServletRequest) {
        // 从请求中获取裁剪后的 jwt数据
        String jwtToken = SecurityUtils.getJwtToken(httpServletRequest);
        return getCoreLoginUser(jwtToken);
    }

    /**
     * 输入 jwt数据，提取出jwt内的 token字段，去缓存中获取对应的 后端登录令牌
     *
     * @param jwtToken jwt数据(前端传递来的 简易用户令牌)
     * @return 缓存中的 后端登录令牌
     */
    public LoginUser getLoginLoginUser(String jwtToken) {
        LoginUser loginLoginUser = null;
        try {
            if (StringUtils.isNotEmpty(jwtToken)) {
                // 获取简易Jwt中的userKey字段(其实就是唯一标识uuid字段)
                String userKey = JwtUtils.getUserKey(jwtToken);
                // 获取简易Jwt中的userId字段(作为LoginUser的缓存key)
                String userId = JwtUtils.getUserId(jwtToken);
                // 构建缓存key后去缓存中查询
                Object cacheObject = redissonUtils.getCacheObject(getTokenCacheKey(userId, userKey));
                if (cacheObject != null) {
                    loginLoginUser = JSON.parseObject(
                            String.valueOf(cacheObject),
                            LoginUser.class);
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return loginLoginUser;
    }

    /**
     * 输入 jwt数据，提取出jwt内的 token字段，去缓存中获取对应的 核心信息令牌
     *
     * @param jwtToken jwt数据(前端传递来的 简易用户令牌)
     * @return 缓存中的 核心信息令牌
     */
    public LoginUser getCoreLoginUser(String jwtToken) {
        LoginUser coreLoginUser = null;
        try {
            if (StringUtils.isNotEmpty(jwtToken)) {
                // 获取简易Jwt中的userId字段(作为LoginUser的缓存key)
                String userId = JwtUtils.getUserId(jwtToken);
                // 构建缓存key后去缓存中查询
                Object cacheObject = redissonUtils.getCacheObject(getUserCoreCacheKey(userId));
                if (cacheObject != null) {
                    coreLoginUser = JSON.parseObject(String.valueOf(cacheObject), LoginUser.class);
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return coreLoginUser;
    }

    /**
     * 将传入的 后端登录令牌 缓存到后端，同样会强制覆盖缓存中相同 key的令牌
     *
     * @param loginLoginUser 后端登录令牌
     */
    public void setLoginLoginUser(LoginUser loginLoginUser) {
        refreshToken(loginLoginUser);
    }

    /**
     * 输入Jwt数据，提取出jwt内的 缓存标识字段，去缓存中删除对应的 后端登录令牌
     *
     * @param jwtToken jwt数据(前端传递来的 简易用户令牌)
     */
    public void delLoginLoginUser(String jwtToken) {
        if (StringUtils.isNotEmpty(jwtToken)) {
            String userKey = JwtUtils.getUserKey(jwtToken);
            String userId = JwtUtils.getUserId(jwtToken);
            redissonUtils.deleteObject(getTokenCacheKey(userId, userKey));
        }
    }

    /**
     * 输入Jwt数据，提取出jwt内的 缓存标识字段，去缓存中删除对应的 核心信息令牌
     *
     * @param jwtToken jwt数据(前端传递来的 简易用户令牌)
     */
    public void delCoreLoginUser(String jwtToken) {
        if (StringUtils.isNotEmpty(jwtToken)) {
            String userId = JwtUtils.getUserId(jwtToken);
            redissonUtils.deleteObject(getUserCoreCacheKey(userId));
        }
    }

    /**
     * 输入Jwt数据，删除在线用户中的记录
     *
     * @param jwtToken
     */
    public void delOnlineUser(String jwtToken) {
        if (StringUtils.isNotEmpty(jwtToken)) {
            String userKey = JwtUtils.getUserKey(jwtToken);
            String userId = JwtUtils.getUserId(jwtToken);
            redissonUtils.deleteObject(CacheConstants.ONLINE_USER_KEY + userId + ":" + userKey);
//            RMapCache<String, String> onlineRMapCache = redissonUtils.getRedissonClient().
//                    getMapCache(CacheConstants.ONLINE_USER_KEY + "RMapCache");
//            String pattern = CacheConstants.ONLINE_USER_KEY + userId + ":*:" + userKey;
//            Set<String> keySet = onlineRMapCache.keySet(pattern);
//            if (keySet != null && keySet.size() > 0) {
//                onlineRMapCache.remove(new ArrayList<>(keySet).get(0));
//            }
        }
    }

    /**
     * 核心信息令牌 不设置自动延期。核心信息令牌只在手动更新时刷新
     */

    /**
     * 自动延期缓存中令牌的有效期。若当前时间和过期时间之间相差小于120分钟，则自动延期令牌有效期
     *
     * @param jwtToken jwt数据
     */
    public void verifyToken(String jwtToken) {
        String userKey = JwtUtils.getUserKey(jwtToken);
        String userId = JwtUtils.getUserId(jwtToken);
        LoginUser loginLoginUser = null;
        Object cacheObject = redissonUtils.getCacheObject(getTokenCacheKey(userId, userKey));
        if (cacheObject != null) {
            loginLoginUser = JSON.parseObject(String.valueOf(cacheObject), LoginUser.class);
            verifyToken(loginLoginUser);
        }

    }

    /**
     * 自动延期缓存中 后端登录令牌 的有效期。若当前时间和过期时间之间相差小于120分钟，则自动延期令牌有效期
     *
     * @param loginLoginUser 后端登录令牌
     */
    public void verifyToken(LoginUser loginLoginUser) {
        long expireTime = loginLoginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginLoginUser);
        }
    }


    /**
     * 将 后端登录令牌 进行缓存。这里会强制用输入的令牌覆盖缓存中相同缓存key的令牌
     *
     * @param loginLoginUser 后端登录令牌
     */
    public void refreshToken(LoginUser loginLoginUser) {
        if (StringUtils.isNotNull(loginLoginUser) && StringUtils.isNotEmpty(loginLoginUser.getToken())) {
            // 刷新最新登录时间
            loginLoginUser.setLoginTime(System.currentTimeMillis());
            // 刷新最新过期时间。为最新登录时间+720分钟
            loginLoginUser.setExpireTime(loginLoginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
            // 制作 后端登录令牌 在缓存时的 key。使用 user_core:UserId:UserKey 作为缓存key
            String tokenCacheKey = getTokenCacheKey(loginLoginUser.getUserid().toString(), loginLoginUser.getToken());
            // 后端登录令牌的过期时间是720分钟
            redissonUtils.setCacheObject(tokenCacheKey, loginLoginUser, expireTime, TimeUnit.MINUTES);
        }
    }

    /**
     * 将 核心信息令牌 进行缓存。这里会强制用输入的令牌覆盖缓存中相同缓存key的令牌
     *
     * @param coreLoginUser 核心信息令牌
     */
    public void refreshCoreLoginUser(LoginUser coreLoginUser) {
        String userCoreCacheKey = getUserCoreCacheKey(coreLoginUser.getUserid().toString());
        redissonUtils.setCacheObject(userCoreCacheKey, coreLoginUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 不断线更新 核心信息令牌
     * 从数据库重新获取并构建最新的 核心信息令牌，直接替换掉缓存中可能过期的 核心信息令牌，实现不下线更新。
     * 注意，ThreadLocal中的 核心信息令牌 在每次请求时都是从缓存中拿出来的，只有更新了缓存中的LoginUser，才能保证ThreadLocal中的是最新的。
     * <p>
     * P.S. 上面是原来的做法。由于新版将 后端登录令牌 和 核心信息令牌拆分开了，所以完全不需要替换就能实现不下线更新。
     * 只需要把这些用户的 核心信息令牌删除即可，因为 ThreadLocal在提取核心信息令牌时，如果不存在，会重新从数据库查询一份的。
     *
     * @param userIds 要更新哪些用户的 核心信息令牌
     */
    public void updateCoreLoginUserCacheOnLine(List<Long> userIds) {
        List<String> coreLoginUserCacheKeys = userIds.stream()
                .map(userId -> getUserCoreCacheKey(userId.toString()))
                .collect(Collectors.toList());
        // 不管有没有，一股脑删了就可以
        redissonUtils.deleteObject(coreLoginUserCacheKeys);
    }

    /**
     * 获取在后端缓存的 后端登录令牌 的key
     *
     * @param userId 登录令牌(不管是前端简易还是后端)的唯一标识。原版使用uuid，但是这样就完全无法在之后查询令牌。
     *               改为使用userId+userKey作为缓存key
     * @return 加上前缀的key
     */
    public String getTokenCacheKey(String userId, String userKey) {
        return ACCESS_TOKEN + userId + ":" + userKey;
    }

    /**
     * 获取后端缓存的用户核心信息的key
     *
     * @param userId userId
     */
    public String getUserCoreCacheKey(String userId) {
        return USER_CORE + userId;
    }

    /**
     * 获取缓存中 在线用户哈希表中 在线用户记录的key
     */
    public String getOnlineCacheKey(String userId, String ipaddr, String userKey) {
        return CacheConstants.ONLINE_USER_KEY + userId + ":" + ipaddr + ":" + userKey;
    }
}
